Manage page meta info in Vue 2.0 components. SSR + Streaming supported. Inspired by react-helmet.
<template>
...
</template>
<script>
export default {
metaInfo: {
title: 'My Example App',
titleTemplate: '%s - Yay!',
htmlAttrs: {
lang: 'en',
amp: undefined
}
}
}
</script>
Table of Contents
Description
vue-meta
is a Vue 2.0 plugin that allows you to manage your app's meta information, much like react-helmet
does for React. However, instead of setting your data as props passed to a proprietary component, you simply export it as part of your component's data using the metaInfo
property.
These properties, when set on a deeply nested component, will cleverly overwrite their parent components' metaInfo
, thereby enabling custom info for each top-level view as well as coupling meta info directly to deeply nested subcomponents for more maintainable code.
Installation
Yarn
$ yarn add vue-meta
NPM
$ npm install vue-meta --save
CDN
Use the links below - if you want a previous version, check the instructions at https://unpkg.com.
Uncompressed:
<script src="https://unpkg.com/vue-meta@1.6.0/lib/vue-meta.js"></script>
Minified:
<script src="https://unpkg.com/vue-meta@1.6.0/lib/vue-meta.min.js"></script>
Usage
Step 1: Preparing the plugin
This step is optional if you don't need SSR and Vue
is available as a global variable. vue-meta
will install itself in this case.
In order to use this plugin, you first need to pass it to Vue.use
- if you're not rendering on the server-side, your JS entry file will suffice. If you are rendering on the server, then place it in a file that runs both on the server and on the client before your root instance is mounted. If you're using vue-router
, then your main router.js
file is a good place:
router.js:
import Vue from 'vue'
import Router from 'vue-router'
import Meta from 'vue-meta'
Vue.use(Router)
Vue.use(Meta)
export default new Router({
...
})
Options
vue-meta
allows a few custom options:
Vue.use(Meta, {
keyName: 'metaInfo',
attribute: 'data-vue-meta',
ssrAttribute: 'data-vue-meta-server-rendered',
tagIDKeyName: 'vmid'
})
If you don't care about server-side rendering, you can skip straight to step 3. Otherwise, continue. :smile:
Step 2: Server Rendering (Optional)
If you have an isomorphic/universal webapp, you'll likely want to render your metadata on the server side as well. Here's how.
Step 2.1: Exposing $meta
to bundleRenderer
You'll need to expose the results of the $meta
method that vue-meta
adds to the Vue instance to the bundle render context before you can begin injecting your meta information. You'll need to do this in your server entry file:
server-entry.js:
import app from './app'
const router = app.$router
const meta = app.$meta()
export default (context) => {
router.push(context.url)
context.meta = meta
return app
}
Step 2.2: Populating the document meta info with inject()
All that's left for you to do now before you can begin using metaInfo
options in your components is to make sure they work on the server by inject
-ing them so you can call text()
on each item to render out the necessary info. You have two methods at your disposal:
Simple Rendering with renderToString()
Considerably the easiest method to wrap your head around is if your Vue server markup is rendered out as a string:
server.js:
app.get('*', (req, res) => {
const context = { url: req.url }
renderer.renderToString(context, (error, html) => {
if (error) return res.send(error.stack)
const bodyOpt = { body: true }
const {
title, htmlAttrs, headAttrs, bodyAttrs, link, style, script, noscript, meta
} = context.meta.inject()
return res.send(`
<!doctype html>
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
<head ${headAttrs.text()}>
${meta.text()}
${title.text()}
${link.text()}
${style.text()}
${script.text()}
${noscript.text()}
</head>
<body ${bodyAttrs.text()}>
${html}
<script src="/assets/vendor.bundle.js"></script>
<script src="/assets/client.bundle.js"></script>
${script.text(bodyOpt)}
</body>
</html>
`)
})
})
If you are using a separate template file, edit your head tag with
<head>
{{{ meta.inject().title.text() }}}
{{{ meta.inject().meta.text() }}}
</head>
Notice the use of {{{
to avoid double escaping. Be extremely cautious when you use {{{
with __dangerouslyDisableSanitizers
.
Streaming Rendering with renderToStream()
A little more complex, but well worth it, is to instead stream your response. vue-meta
supports streaming with no effort (on it's part :stuck_out_tongue_winking_eye:) thanks to Vue's clever bundleRenderer
context injection:
server.js
app.get('*', (req, res) => {
const context = { url: req.url }
const renderStream = renderer.renderToStream(context)
renderStream.once('data', () => {
const bodyOpt = { body: true }
const {
title, htmlAttrs, headAttrs, bodyAttrs, link, style, script, noscript, meta
} = context.meta.inject()
res.write(`
<!doctype html>
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
<head ${headAttrs.text()}>
${meta.text()}
${title.text()}
${link.text()}
${style.text()}
${script.text()}
${noscript.text()}
</head>
<body ${bodyAttrs.text()}>
`)
})
renderStream.on('data', (chunk) => {
res.write(chunk)
})
renderStream.on('end', () => {
res.end(`
<script src="/assets/vendor.bundle.js"></script>
<script src="/assets/client.bundle.js"></script>
${script.text(bodyOpt)}
</body>
</html>
`)
})
renderStream.on('error', (error) => res.status(500).end(`<pre>${error.stack}</pre>`))
})
Step 3: Start defining metaInfo
In any of your components, define a metaInfo
property:
App.vue:
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
metaInfo: {
title: 'Default Title',
titleTemplate: '%s | My Awesome Webapp'
}
}
</script>
Home.vue
<template>
<div id="page">
<h1>Home Page</h1>
</div>
</template>
<script>
export default {
name: 'Home',
metaInfo: {
title: 'My Awesome Webapp',
titleTemplate: null
}
}
</script>
About.vue
<template>
<div id="page">
<h1>About Page</h1>
</div>
</template>
<script>
export default {
name: 'About',
metaInfo: {
title: 'About Us'
}
}
</script>
Recognized metaInfo
Properties
title
(String)
Maps to the inner-text value of the <title>
element.
{
metaInfo: {
title: 'Foo Bar'
}
}
<title>Foo Bar</title>
titleTemplate
(String | Function)
The value of title
will be injected into the %s
placeholder in titleTemplate
before being rendered. The original title will be available on metaInfo.titleChunk
.
{
metaInfo: {
title: 'Foo Bar',
titleTemplate: '%s - Baz'
}
}
<title>Foo Bar - Baz</title>
The property can also be a function (from v1.2.0):
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} - Site Title` : 'Site Title';
}
htmlAttrs
(Object)
Each key:value maps to the equivalent attribute:value of the <html>
element.
{
metaInfo: {
htmlAttrs: {
foo: 'bar',
amp: undefined
}
}
}
<html foo="bar" amp></html>
headAttrs
(Object)
Each key:value maps to the equivalent attribute:value of the <head>
element.
{
metaInfo: {
headAttrs: {
foo: 'bar'
}
}
}
<head foo="bar"></head>
bodyAttrs
(Object)
Each key:value maps to the equivalent attribute:value of the <body>
element.
{
metaInfo: {
bodyAttrs: {
bar: 'baz'
}
}
}
<body bar="baz">Foo Bar</body>
base
(Object)
Maps to a newly-created <base>
element, where object properties map to attributes.
{
metaInfo: {
base: { target: '_blank', href: '/' }
}
}
<base target="_blank" href="/">
meta
([Object])
Each item in the array maps to a newly-created <meta>
element, where object properties map to attributes.
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
}
}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Since v1.5.0, you can now set up meta templates that work similar to the titleTemplate:
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{
'property': 'og:title',
'content': 'Test title',
'template': chunk => `${chunk} - My page`,
'vmid': 'og:title'
}
]
}
}
<meta charset="utf-8">
<meta name="og:title" property="og:title" content="Test title - My page">
link
([Object])
Each item in the array maps to a newly-created <link>
element, where object properties map to attributes.
{
metaInfo: {
link: [
{ rel: 'stylesheet', href: '/css/index.css' },
{ rel: 'favicon', href: 'favicon.ico' }
]
}
}
<link rel="stylesheet" href="/css/index.css">
<link rel="favicon" href="favicon.ico">
style
([Object])
Each item in the array maps to a newly-created <style>
element, where object properties map to attributes.
{
metaInfo: {
style: [
{ cssText: '.foo { color: red }', type: 'text/css' }
]
}
}
<style type="text/css">.foo { color: red }</style>
script
([Object])
Each item in the array maps to a newly-created <script>
element, where object properties map to attributes.
{
metaInfo: {
script: [
{ src: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.js', async: true, defer: true }
],
}
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" async="true" defer="true"></script>
:warning: You have to disable sanitizers so the content of innerHTML
won't be escaped. Please refer to __dangerouslyDisableSanitizers section below for more info on related risks.
{
metaInfo: {
script: [
{ innerHTML: '{ "@context": "http://schema.org" }', type: 'application/ld+json' }
],
__dangerouslyDisableSanitizers: ['script'],
}
}
<script type="application/ld+json">{ "@context": "http://schema.org" }</script>
If your browser doesn't support defer
or any other reason, you want to put <script>
before </body>
, use body
.
{
metaInfo: {
script: [
{ innerHTML: 'console.log("I am in body");', type: 'text/javascript', body: true }
]
}
}
noscript
([Object])
Each item in the array maps to a newly-created <noscript>
element, where object properties map to attributes.
{
metaInfo: {
noscript: [
{ innerHTML: 'This website requires JavaScript.' }
]
}
}
<noscript>This website requires JavaScript.</noscript>
__dangerouslyDisableSanitizers
([String])
By default, vue-meta
sanitizes HTML entities in every property. You can disable this behaviour on a per-property basis using __dangerouslyDisableSantizers
. Just pass it a list of properties you want sanitization to be disabled on:
{
metaInfo: {
title: '<I will be sanitized>',
meta: [{ vmid: 'description', name: 'description', content: '& I will not be <sanitized>'}],
__dangerouslyDisableSanitizers: ['meta']
}
}
<title><I will be sanitized></title>
<meta vmid="description" name="description" content="& I will not be <sanitized>">
:warning: Using this option is not recommended unless you know exactly what you are doing. By disabling sanitization, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
__dangerouslyDisableSanitizersByTagID
({[String]})
Provides same functionality as __dangerouslyDisableSanitizers
but you can specify which property for which tagIDKeyName
's sanitization should be disabled. It expects an object with the vmid's as key and an array with property names value:
{
metaInfo: {
title: '<I will be sanitized>',
meta: [{ vmid: 'description', name: 'still-&-sanitized', content: '& I will not be <sanitized>'}],
__dangerouslyDisableSanitizersByTagID: { description: ['content'] }
}
}
<title><I will be sanitized></title>
<meta vmid="description" name="still-&-sanitized" content="& I will not be <sanitized>">
:warning: Using this option is not recommended unless you know exactly what you are doing. By disabling sanitization, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
changed
(Function)
Will be called when the client metaInfo
updates/changes. Receives the following parameters:
newInfo
(Object) - The new state of the metaInfo
object.addedTags
([HTMLElement]) - a list of elements that were added.removedTags
([HTMLElement]) - a list of elements that were removed.
this
context is the component instance changed
is defined on.
{
metaInfo: {
changed (newInfo, addedTags, removedTags) {
console.log('Meta info was updated!')
}
}
}
How metaInfo
is Resolved
You can define a metaInfo
property on any component in the tree. Child components that have metaInfo
will recursively merge their metaInfo
into the parent context, overwriting any duplicate properties. To better illustrate, consider this component heirarchy:
<parent>
<child></child>
</parent>
If both <parent>
and <child>
define a title
property inside metaInfo
, then the title
that gets rendered will resolve to the title
defined inside <child>
.
Lists of Tags
When specifying an array in metaInfo
, like in the below examples, the default behaviour is to simply concatenate the lists.
Input:
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{ name: 'description', content: 'foo' }
]
}
}
{
metaInfo: {
meta: [
{ name: 'description', content: 'bar' }
]
}
}
Output:
<meta charset="utf-8">
<meta name="description" content="foo">
<meta name="description" content="bar">
This is not what we want, since the meta description
needs to be unique for every page. If you want to change this behaviour such that description
is instead replaced, then give it a vmid
:
Input:
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{ vmid: 'description', name: 'description', content: 'foo' }
]
}
}
{
metaInfo: {
meta: [
{ vmid: 'description', name: 'description', content: 'bar' }
]
}
}
Output:
<meta charset="utf-8">
<meta vmid="description" name="description" content="bar">
While solutions like react-helmet
manage the occurrence order and merge behaviour for you automatically, it involves a lot more code and is therefore prone to failure in some edge-cases, whereas this method is almost bulletproof because of its versatility; at the expense of one tradeoff: these vmid
properties will be rendered out in the final markup (vue-meta
uses these client-side to prevent duplicating or overriding markup). If you are serving your content GZIP'ped, then the slight increase in HTTP payload size is negligible.
Performance
On the client, vue-meta
batches DOM updates using requestAnimationFrame
. It needs to do this because it registers a Vue mixin that subscribes to the beforeMount
lifecycle hook on all components in order to be notified that renders have occurred and data is ready. If vue-meta
did not batch updates, the DOM meta info would be re-calculated and re-updated for every component on the page in quick-succession.
Thanks to batch updating, the update will only occurr once - even if the correct meta info has already been compiled by the server. If you don't want this behaviour, see below.
How to prevent the update on the initial page render
Add the data-vue-meta-server-rendered
attribute to the <html>
tag on the server-side:
<html data-vue-meta-server-rendered>
...
vue-meta
will check for this attribute whenever it attempts to update the DOM - if it exists, vue-meta
will just remove it and perform no updates. If it does not exist, vue-meta
will perform updates as usual.
Note: While this may seem verbose, it is intentional. Having vue-meta
handle this for you automatically would limit interoperability with other server-side programming languages. If you use PHP to power your server, for example, you might also have meta info handled on the server already and want to prevent this extraneous update.
FAQ
Here are some answers to some frequently asked questions.
How do I use component props and/or component data in metaInfo
?
Easy. Instead of defining metaInfo
as an object, define it as a function and access this
as usual:
Post.vue:
<template>
<div>
<h1>{{{ title }}}</h1>
</div>
</template>
<script>
export default {
name: 'post',
props: ['title'],
data () {
return {
description: 'A blog post about some stuff'
}
},
metaInfo () {
return {
title: this.title,
meta: [
{ vmid: 'description', name: 'description', content: this.description }
]
}
}
}
</script>
PostContainer.vue:
<template>
<div>
<post :title="title"></post>
</div>
</template>
<script>
import Post from './Post.vue'
export default {
name: 'post-container',
components: { Post },
data () {
return {
title: 'Example blog post'
}
}
}
</script>
How do I populate metaInfo
from the result of an asynchronous action?
vue-meta
will do this for you automatically when your component state changes.
Just make sure that you're using the function form of metaInfo
:
{
data () {
return {
title: 'Foo Bar Baz'
}
},
metaInfo () {
return {
title: this.title
}
}
}
Check out the vuex-async example for a far more detailed demonstration if you have doubts.
Credit & Thanks for this feature goes to Sébastien Chopin.
Why doesn't vue-meta
support jsnext:main
?
Originally, it did - however, it caused problems. Essentially, Vue does not support jsnext:main
, and does not introspect for the default
property that is transpiled from the ES2015 source, thus breaking module resolution.
Given that jsnext:main
is a non-standard property that won't stick around for long, and vue-meta
is bundled into one file with no dynamic module internals as well as the fact that if you're using vue-meta
, you're 99.9% likely to not be using it conditionally - the decision has been made to drop support for it entirely.
If this were not the case, you would have to instruct Babel to convert default
imports to the proper commonjs module syntax via a plugin, which is not ideal since many users in the Vue landscape write their code in TypeScript, not Babel.
Examples
To run the examples; clone this repository & run npm install
in the root directory, and then run npm run dev
. Head to http://localhost:8080.